Weaving maps of multivariate data
Weave patterns where each ‘thread’ or ‘strand’ represents a different attribute that can be independently symbolised in a map.
from Chaves LF, MD Friberg, LA Hurtado, R Marín Rodríguez, D O’Sullivan, and LR Bergmann. 2021 (online first). Trade, uneven development and people in motion: Used territories and the initial spread of COVID-19 in Mesoamerica and the Caribbean. Socio-Economic Planning Sciences.
This was done with SVG symbol fills in QGIS, and was (very!) fiddly to produce.
Motivation and other approaches
Increasingly, we deal with highly multivariate data.
Many approaches can be used to visualise these spatially, but it’s always challenging. We may resort to small multiple displays, where each attribute is presented as an individual (small) map. Or perhaps more often, we flip back and forward among many layers in a GIS.
Wouldn’t it be nice to see multiple attributes together?! Perhaps to be able to identify patterns across more than one attribute in combination.
We’re not the first to think this, so there are plenty of approaches already around…
Small multiples
This is sf’s default plot output for a dataset.
plot(region)
Bivariate choropleths
E.g., using Jan Caha’s QGIS plugin which implements an approach described by Joshua Stevens in this post
Bivariate choropleth made in QGIS
Trivariate choropleths
Mixing three colours is hard, but e.g., the tricolore package can do this…
eth_mix <- tricolore::Tricolore(
region, p1 = "pEuropean", p2 = "pMaori", p3 = "pAsian", breaks = 5
)
region$eth_mix_tri <- eth_mix$rgb
ggplot(region) +
geom_sf(aes(fill = eth_mix_tri)) +
scale_fill_identity() +
annotation_custom(
grob = ggplotGrob(eth_mix$key + labs(L = 'Pākehā', T = 'Māori', R = 'Asian')),
xmin = 1.7465e6, xmax = 1.7535e6, ymin = 5.9075e6, ymax = 5.9125e6)
Symbols over choropleths
Choropleth with symbols made in QGIS
Multivariate symbols
The classic example is Dorling’s Chernoff faces map of the UK 1987 election.
Figure 8.10 Dorling D. 2012. The visualisation of spatial social structure. Chichester, England: John Wiley & Sons.
Multi-element patterns
There are many variations on this idea, but perhaps the most common is a categorical dot map.
This example made using tmap and data preparation code from James Smythe’s cultureofinsight blog
dot map of ethnic composition
A woven map
weave_unit <- get_biaxial_weave_unit(spacing = 200, type = "twill", n = 3,
aspect = 0.6, ids = "ab|cd",
crs = st_crs(region))
fabric <- weave_layer(weave_unit, region, angle = 30)Split the data by the id so that it is convenient to symbolise them separately.
layers <- fabric %>% split(as.factor(fabric$id))- Each
idvalue can be symbolised separately using symbolisation the data can support - We can also plot the region data as a choropleth if desired
tm_shape(region, name = "Dose 2 uptake") +
tm_fill(col = "dose2_uptake", palette = "inferno", style = "cont",
title = "Dose 2 per 1000", id = "SA22018_V1_00_NAME") +
tm_shape(layers$a, name = "Pākehā") +
tm_fill(col = "pEuropean", palette = "Greys", title = "% Pākehā", n = 3,
id = "SA22018_V1_00_NAME", popup.vars = c("pEuropean")) +
tm_shape(layers$b, name = "Māori") +
tm_fill(col = "pMaori", palette = "Reds", title = "% Māori", n = 3,
id = "SA22018_V1_00_NAME", popup.vars = c("pMaori")) +
tm_shape(layers$c, name = "Pasifika") +
tm_fill(col = "pPacific", palette = "Purples", title = "% Pasifika", n = 3,
id = "SA22018_V1_00_NAME", popup.vars = c("pPacific")) +
tm_shape(layers$d, name = "Asian") +
tm_fill(col = "pAsian", palette = "Greens", title = "% Asian", n = 3,
id = "SA22018_V1_00_NAME", popup.vars = c("pAsian"))Implementing woven maps
- Regular rectangular or hex grids of points generated by geospatial tools
tiling a weave unit
What repeatable units can tile across such grids to give the appearance of a woven pattern?
Turns out this is of interest to mathematicians (Grünbaum and Shephard 1985, 1986), who call such tileable elements the fundamental blocks of isonemal fabrics
Anyway… we have proof-of-concept R tools to make weave patterns:
- Make a weave unit
- Tile the map area with the weave unit
- Export to a multi-layer GPKG
- Symbolise the weave elements as desired in any tool
Weave units (or fundamental blocks)
Biaxial weaves
Plain weaves
Traditional weave patterns with threads in two directions, the warp and the weft. We generate these using matrix multiplication (cf. Glassner 2002)
Simplest is a plain weave
rect11_unit <- ## plain weave example
get_biaxial_weave_unit(spacing = 300, type = "plain",
ids = "a|b", crs = st_crs(region))
rect11_unit$primitive %>% plot(border = NA, main = "Plain weave unit")
This could be useful if clearly distinct palettes were used in the warp and weft elements. More useful is if we change the aspect to the warp and weft elements so we can distinguish directions:
rect11_unit <- ## plain weave example
get_biaxial_weave_unit(spacing = 300, aspect = 0.8,
ids = "a|b", crs = st_crs(region))
rect11_unit$primitive %>% plot(border = NA, main = "Plain weave unit, with directions")
More threads, more colours
This is highly customisable:
rect32_unit <-
get_biaxial_weave_unit(spacing = 300, aspect = sqrt(0.5),
ids = "abc|de", crs = st_crs(region))
rect32_unit$primitive %>% plot(border = NA, main = "Plain weave, 3 warp and 2 weft colours")
Missing threads
We can even leave gaps or duplicate threads
rect34_unit <- ## plain weave example
get_biaxial_weave_unit(spacing = 150, aspect = 0.8,
ids = "ab-|cc-d", crs = st_crs(region))
rect34_unit$primitive %>% plot(border = NA, main = "Complex plain weave with missing strands")
Twill weaves and basket weaves
twill_unit <-
get_biaxial_weave_unit(spacing = 150, type = "twill", n = c(2, 2),
aspect = 0.7, ids = "a|b", crs = st_crs(region))
twill_unit$primitive %>% plot(border = NA, main = "2 over 2 under twill weave")
basket_unit <-
get_biaxial_weave_unit(spacing = 150, type = "basket", n = 2,
aspect = 0.7, ids = "a|b", crs = st_crs(region))
basket_unit$primitive %>% plot(border = NA, main = "2 over 2 under basket weave")
Other weaves
We can generate any biaxial weavable pattern, even crazy ones!
this_unit <-
get_biaxial_weave_unit(spacing = 37.5, type = "this", ids = ids,
crs = st_crs(region))
this_unit$primitive %>% plot(border = NA, main = "Pattern from Glassner 2002")
Triaxial weave units
Hexagonal
We can also make weaves with threads running in 3 directions. This example is based on a hexagonal tileable unit, and can also allow for more than one thread in each direction
hex_unit <- ## hex example
get_triaxial_weave_unit(spacing = 600, margin = 2,
ids = "a|bc|def", type = "hex", crs = st_crs(region))
hex_unit$tile %>% plot(col = "white", border = "black",
main = "Hex-based triangular weave")
hex_unit$primitive %>% plot(border = NA, add = TRUE)
A cube is another option, although this produces some odd 3D effects when tiled
cube_unit <-
get_triaxial_weave_unit(spacing = 600, ids = "ab|cd|ef", margin = 2,
type = "cube", crs = st_crs(region))
cube_unit$primitive %>% plot(border = NA,
main = "Mad weave (so called) with two colours in each direction")
Weave a map
As another example here are some data from Ellis et al. 2021, which aim to show change over 1000s of years in human-affected landcover globally.
anthromes <- st_read("data/nz-anthromes-dgg.gpkg")
tmap_mode("plot")
tm_shape(anthromes) +
tm_fill(col = paste("class", seq(2000, 1800, -100), sep = ""),
palette = anthrome_colors(), title = "Anthrome") +
tm_layout(panel.labels = c("2000", "1900", "1800"),
legend.outside = TRUE)
We make a weave unit and tile it so we can put the three snapshots in a single map.
tri_unit <- get_triaxial_weave_unit(spacing = 8000, ids = "a|b|c", type = "hex",
crs = st_crs(anthromes))
fabric2 <- weave_layer(tri_unit, anthromes, angle = 15)Some tidying up needed for the web map hover labelling
fabric2 <- st_read("data/fabric2-raw.gpkg") %>%
mutate(label = if_else(id == "a",
paste("1800", class1800), # make labels that include
ifelse(id == "b", # year and anthrome class
paste("2000", class2000),
paste("1900", class1900)))) %>%
select(21, 1:19) # and put them in 1st column
layers2 <- fabric2 %>% split(as.factor(fabric2$id))
tmap_mode("view")tm_shape(layers2$a, name = "1800") +
tm_fill(col = "class1800", palette = anthrome_colors(), legend.show = FALSE) +
tm_shape(layers2$c, name = "1900") +
tm_fill(col = "class1900", palette = anthrome_colors(), legend.show = FALSE) +
tm_shape(layers2$b, name = "2000") +
tm_fill(col = "class2000", palette = anthrome_colors(), title = "Anthrome")